package dungeon;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import basicPackage.Direction;
import basicPackage.Temperature;
import basicPackage.TemperatureScale;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;


/**
 * This class represents a square of the game board. A square has a temperature,
 * a humidity and the quality of being inherently slippery or not.
 * A square can border up to 6 other dungeon objects.
 * 
 * @invar	The humidity percentage is registered with 2 decimals.
 * 			| getHumidityPercentage.scale() = 2
 * @invar	The humidity percentage is a valid humidity percentage
 * 			| isValidHumidityPercentage(getHumidityPercentage())
 * @invar	The square cannot have a door in the floor.
 * 			|	! this.getBorder(Direction.FLOOR) instanceof Door
 * @invar	The square may not border more than 3 doors.
 * 			| this.getNbOfBorderingDoors() <= 3
 * @invar	The square must border at least one door or wall.
 * 			| this.getNbOfBorderingObstacles() >= 1
 * 
 * @author 	Frederik Moyaert and Laurens Vanderstraeten
 * 			2de Bach. Burgerlijk Ingenieur, wtkcws
 */
public abstract class Square extends DungeonObject {

//	/**
//	 * Initialize a new square, with all the parameters given: 
//	 * temperature, minimum temperature, maximum temperature, 
//	 * humidity and if the square is inherently slippery.
//	 * 
//	 * @param 	temperature	
//	 * 			The temperature of the new square.
//	 * @param 	minTemperature
//	 * 			The minimum temperature of the new square.
//	 * @param 	maxTemperature
//	 * 			The maximum temperature of the new square.
//	 * @param 	humidity		
//	 * 			The humidity of the new square.
//	 * @param 	inherentlySlippery
//	 * 			The inherently slippery state of the new square.
//	 * 
//	 * @pre		The given humidity percentage must be a valid humidity percentage.
//	 * 			| isValidHumidityPercentage(humidityPercentage)
//	 * @post	The new minimum temperature of this square is equal to the given minimum temperature.
//	 * 			| new.getMinimumTemperature() == minimumTemperature
//	 * @post	The new maximum temperature of this square is equal to the given maximum temperature.
//	 * 			| new.getMaximumTemperature() == maximumTemperature
//	 * @post	The new temperature of this square is equal to the given temperature.
//	 * 			| new.getTemperature() == temperature
//	 * @post	The new humidity percentage of this square is equal to the given humidity percentage.
//	 * 			| new.getHumidityPercentage() == humidityPercentage
//	 * @post	This square will border one wall that is the floor.
//	 * 			| new.getNbOfBorders() == 1 && new.getBorder(Direction.FLOOR) instanceof Wall
//	 * @throws 	IllegalArgumentException
//	 * 			If the minimum temperature is null.
//	 * 			| !isValidMinTemperature(minTemperature)
//	 * @throws	IllegalArgumentException			
//	 * 			If the maximum temperature is null or not more than the 
//	 * 			minimum temperature.
//	 * 			| !isValidMaxTemperature(maxTemperature)
//	 * @throws	IllegalArgumentException
//	 * 			If the temperature is null or not between the minimum and 
//	 * 			maximum temperature.
//	 * 			| !isValidTemperature(temperature)
//	 */
//	@Raw
//	public Square(Temperature temperature, BigDecimal humidity,
//				boolean inherentlySlippery) throws IllegalArgumentException{
//		super(inherentlySlippery);
//		setTemperature(temperature);
//		setHumidityPercentage(humidity);
//	}
	
	/**
	 * Initialize a new square, with the basic parameters given: temperature, 
	 * humidity and if the square is inherently slippery.
	 * 
	 * @param 	temperature	
	 * 			The temperature of the new square.
	 * @param 	humidity		
	 * 			The humidity of the new square.
	 * @param 	inherentlySlippery
	 * 			The inherently slippery state of the new square.
	 * @param	directions
	 * 			The directions in which default borders should be set.
	 * @pre		The given humidity percentage must be a valid humidity percentage.
	 * 			| isValidHumidityPercentage(humidityPercentage)
	 * @post	The new temperature of this square is equal to the given temperature.
	 * 			| new.getTemperature() == temperature
	 * @post	The new humidity percentage of this square is equal to the given humidity percentage.
	 * 			| new.getHumidityPercentage() == humidityPercentage
	 * @post	This square will have default borders in the given directions.
	 * @throws 	IllegalArgumentException
	 * 			If the temperature is not valid.
	 * 			| !isValidTemperature(temperature)		
	 */
	@Raw
	public Square(Temperature temperature,BigDecimal humidity,
				boolean inherentlySlippery,Direction...directions) throws IllegalArgumentException {
		super(inherentlySlippery);
		this.temperature = temperature;
		setHumidityPercentage(humidity);
		setDefaultBorders(directions);
	}

	
	/**
	 * Return a copy of the temperature of the square.
	 * 
	 * @return		A copy of the temperature of the square.
	 */
	@Basic
	public Temperature getTemperature(){
		return temperature;
	}
	
	public abstract boolean isValidTemperature(Temperature temperature);
		
	/**
	 * Set a temperature for the square.
	 * 
	 * @param 	temperature		
	 * 			The new temperature for the square.
	 * @throws 	IllegalArgumentException
	 * 			If the temperature is not a valid value for the temperature.
	 * 			| !isValidTemperature()
	 * 
	 * @result	The new temperature of the square is temperature.
	 * 			| new.getTemperature() == temperature
	 */
	@Raw
	protected void setTemperature(Temperature temperature){
		if (!isValidTemperature(temperature))
			throw new IllegalArgumentException
			("The temperature isn't a valid temperature value.");
		this.temperature = temperature;
	}
	
	/**
	 * The temperature of the square.
	 */
	private Temperature temperature;
	
	/**
	 * Return the number of the humidity percentage decimals has that applies to
	 * all squares.
	 * 
	 * @return 	The number of the humidity percentage decimals is 2.
	 * 			| result == 2
	 */
	@Basic @Raw @Immutable
	public static int getHumidityPercentageDecimals() {
		return humidityPercentageDecimals;
	}

	/**
	 * Variable for registering the number of decimals the humidity percentage
	 * has that applies to all squares.
	 */
	private static final int humidityPercentageDecimals = 2;


	/**
	 * Return the minimal humidity percentage that applies to all squares.
	 */
	@Basic @Immutable @Raw
	public static BigDecimal getMinHumidityPercentage() {
		return minHumidityPercentage;
	}

	/**
	 * Variable for registering the minimal humidity percentage
	 */
	private static final BigDecimal minHumidityPercentage = new BigDecimal(0);


	/**
	 * Return the maximal humidity percentage that applies to all squares.
	 */
	@Basic @Immutable @Raw
	public static BigDecimal getMaxHumidityPercentage() {
		return maxHumidityPercentage;
	}

	/**
	 * Variable for registering the maximal humidity percentage
	 */
	private static final BigDecimal maxHumidityPercentage = new BigDecimal(100);


	/**
	 * Return the humidity percentage of this square.
	 * 
	 * @return	The returned humidity percentage has decimals equal to the number of 
	 * 			humidity percentage decimals.
	 * 			| result.getScale() == getHumidityPercentageDecimals
	 */
	@Basic @Raw
	public BigDecimal getHumidityPercentage() {
		return humidityPercentage;
	}

	/**
	 * Check whether the given humidity percentage is a valid humidity
	 * percentage for any square.
	 * 
	 * @param 	humidityPercentage
	 * 			The humidity percentage to be checked.
	 * @return 	False if the given humidity percentage either not effective, has more or less decimals 
	 *         	then the humidity percentage decimals or is not a value between the minimal humidity 
	 *         	percentage and the	maximal humidity percentage.
	 *         	| result == ((humidityPercentage != null)&& humidityPercentage.Scale() == 2 && (getMinHumidityPercentage().compareTo(humidityPercentage) <= 0)
	 *         	| && (humidityPercentage.compareTo(getMaxHumidityPercentage()) <= 0) 
	 */
	@Raw
	public boolean isValidHumidityPercentage(BigDecimal humidityPercentage) {
		return ((humidityPercentage != null) && humidityPercentage.scale() == 2 && getMinHumidityPercentage().compareTo(humidityPercentage) <= 0
				&& (humidityPercentage.compareTo(getMaxHumidityPercentage()) <= 0));
	}
	
		
	/**
	 * Set the humidity percentage of this square to the given humidity
	 * percentage.
	 * 
	 * @param	humidityPercentage
	 *       	The percentage to be registered as the humidity percentage of
	 *			this square.
	 * @pre 	The given percentage must be a valid humidity percentage for a
	 *      	square. 
	 *      	| isValidHumidityPercentage(humidityPercentage)
	 * @post 	The new humidity percentage of this square is equal to the given
	 *       	humidity percentage. 
	 *       	| new.getHumidityPercentage() == humidityPercentage
	 */
	@Raw
	protected void setHumidityPercentage(BigDecimal humidityPercentage) {
		assert isValidHumidityPercentage(humidityPercentage);
		this.humidityPercentage = humidityPercentage;
	}	
	
	/**
	 * Variable for registering the humidity percentage.
	 */
	private BigDecimal humidityPercentage;
	
	/**
	 * 
	 * Set whether this square is inherently slippery or not.
	 * 
	 * @param 	inherentlySlippery
	 *         	Whether the square is to be inherently slippery or not.
	 * @post 	The new inherently slippery of this square is equal to the given
	 *       	inherently slippery. 
	 *       	| isInherentlySlippery() == inherentlySlippery
	 */
	@Raw
	void setInherentlySlippery(Boolean inherentlySlippery){
		super.setInherentlySlippery(inherentlySlippery);
	}
	
	/**
	 * Check whether the given direction is valid or not.
	 * 
	 * @param 	direction
	 * 			The direction to be checked
	 * @return	True if and only if the direction is not null.
	 * 			| if (direction != null) 
	 *        	| then result == true 
	 */
	@Raw @Immutable
	public static boolean isValidDirection(Direction direction){
		return direction != null;
	}
	
	/**
	 * Check whether the given square can border this square in the given direction.
	 * 
	 * @param 	direction
	 * 			The direction that needs to be checked from this square to the other square.
	 * @param 	square
	 * 			The square that can or cannot border this square in the given direction.
	 */
	public abstract boolean isValidSquareToAdd(Direction direction, Square square);
	
	/**
	 * Check whether the given obstacle can border this square in the given direction.
	 * 
	 * @param 	direction
	 * 			The direction that needs to be checked from this square to the obstacle.
	 * @param 	obstacle
	 * 			The obstacle that can or cannot border this square in the given direction.
	 * @return	False if the direction is null.
	 * 			| if (!isValidDirection(direction))
	 * 			|	then result == false
	 * @return	False if the obstacle is null.
	 * 			| if (obstacle == null)
	 * 			|	then result == false
	 * @return	False if an obstacle will be set that already borders this square.
	 * 			| if (obstacle.containsSquare(this))
	 * 			|	then result == false
	 * @return	False if the given obstacle already borders 2 squares.
	 * 			| if (obstacle.getSecondSquare() != null)
	 * 			|	then result == false
	 * @return	False if the obstacle has already borders a square that doesn't border the obstacle in the correct direction.
	 * 			| if(obstacle.getFirstSquare() != null && obstacle.getFirstSquare().getBorder(direction.getOppositeDirection()) != obstacle)
	 *			|	then result == false
	 */
	public boolean isValidObstacleToAdd(Direction direction, Obstacle obstacle){
		if(! isValidDirection(direction))
			return false;
		if (obstacle == null)
			return false;
		if (obstacle.containsSquare(this))
			return false;
		if(obstacle.getSecondSquare() != null)
			return false;
		if(obstacle.getFirstSquare() != null && obstacle.getFirstSquare().getBorder(direction.getOppositeDirection()) != obstacle)
			return false;
		return true;
	}

	/**
	 * Check whether the dungeon object in the given direction can be deleted.
	 * 
	 * @param 	direction	
	 * 			The direction in which the dungeon object can or can't be deleted.
	 * @return	True if and only if the direction is not null and if the result of disconnecting will be 
	 * 				that there is no more bordering obstacles left in the given direction.	
	 */
	public abstract boolean isValidDirectionToDelete(Direction direction);
		
	/**
	 * Get the number of doors that border this square.
	 * 
	 * @return	The number of bordering doors.
	 * 			| result <= 3
	 */
	public int getNbOfBorderingDoors(){
		Collection<DungeonObject> borders = this.getBorders().values();
		int numberOfBorderingDoors = 0;
		for (DungeonObject border:borders){
			if (border instanceof Door)
				numberOfBorderingDoors++;
		}
		return numberOfBorderingDoors;
	}
	
	/**
	 * Get the number of obstacles (doors or walls) that border this square.
	 * 
	 * @return	The number of bordering obstacles.
	 * 			| result > 0
	 */
	public int getNbOfBorderingObstacles(){
		Collection<DungeonObject> borders = this.getBorders().values();
		int numberOfBorderingObstacles = 0;
		for (DungeonObject border: borders){
			if( border instanceof Obstacle)
				numberOfBorderingObstacles++;
		}
		return numberOfBorderingObstacles;
	}
	
	/**
	 * Get the number of borders this square has different from null.
	 * 
	 * @return	The number of borders.
	 */
	public int getNbOfBorders(){
		int nbOfBorders=0;
		for(DungeonObject border:this.getBorders().values()){
			if (border != null)
				nbOfBorders++;
		}
		return nbOfBorders;
	}
	
	/**
	 * Check whether this square has a border in the given direction.
	 * 
	 * @param	direction
	 *        	The direction of the border to be checked.
	 * @return 	True if and only if this square has a border square in the given
	 *         	direction
	 *         	| if (isValidDirection(direction)) 
	 *         	| 	then result == (getBorder(direction) != null)
	 */
	public boolean hasBorder(Direction direction) {
		if (isValidDirection(direction))
			return getBorder(direction) != null;
		return false;
	}

	/**
	 * Return the dungeon object that borders this square in the given direction.
	 * 
	 * @param direction	The direction in which this square borders the dungeon object.
	 * @return			The dungeon object that borders this square in the given direction.
	 */
	public DungeonObject getBorder(Direction direction) {
		return getBorders().get(direction);
	}
	/**
	 * Return the square which borders this square in the given direction, separated by an Obstacle or not.
	 * 
	 * @param 	direction
	 * 			The direction of the bordering square to be retrieved.
	 * @return	The square which borders this square in the given direction, separated by an Obstacle or not.
	 * 			| if(getBorder(direction) instanceof Square)
	 *			|	result == getBorder(direction);
	 *			| if(getBorder(direction) instanceof Obstacle){
	 *			|	result == getBorder(direction).getOtherSquare(this);
	 *			| else then
	 *			|	result == null
	 */
	public Square getBorderingSquare(Direction direction) {
		if(getBorder(direction) instanceof Square)
			return (Square)getBorder(direction);
		if(getBorder(direction) instanceof Obstacle){
			return ((Obstacle)getBorder(direction)).getOtherSquare(this);
		}
		return null;
	}
	
	/**
	 * Return a list of squares that border this square, seperated by an obstacle or not.
	 * 
	 * @return	The list of bordering squares
	 */
	public List<Square> getBorderingSquares(){
		List<Square> result = new ArrayList<Square>();
		for(DungeonObject border: getBorders().values()){
			if(border instanceof Square)
				result.add(((Square)border));
			if (border instanceof Obstacle && ((Obstacle)border).getOtherSquare(this) != null)
				result.add(((Obstacle)border).getOtherSquare(this));				
		}
		return result;
	}
	
	/**
	 * Return the map with key the direction and value the border of this square. 
	 * 
	 * @return	A map with all the possible directions and their according borders.
	 */
	@Basic
	public Map<Direction,DungeonObject> getBorders() {
		Map<Direction,DungeonObject> borders = this.borders;
		return borders;
	}


	/**
	 * Set the border of this square in the given direction.
	 * 
	 * @param	direction
	 *       	The direction for the border to be set to.
	 * @param	dungeonObject
	 *       	The dungeon object that borders this square in the given direction.
	 *       	If the dungeon object is null, it means that a border in the given direction is initialized or deleted.
	 * @post	The new border in the given direction is set to the given border.
	 *     	  	| new.getBorder(direction) == dungeonObject
	 * @throws	IllegalArgumentException
	 * 			If the direction is null.
	 * 			| ! isValidDirection(direction)
	 */
	@Raw
	protected void setBorder(Direction direction, DungeonObject dungeonObject) {
		if(! isValidDirection(direction))
			throw new IllegalArgumentException();
		borders.put(direction, dungeonObject);
	}

	/**
	 * Set the default borders for this square.
	 * 
	 * @param 	directions
	 * 			The directions in which the default borders will be set.
	 */
	@Raw
	protected abstract void setDefaultBorders(Direction... directions);
	
	/**
	 * A variable map that contains this squares' immediate borders in all possible directions.
	 */
	private Map<Direction,DungeonObject> borders = new HashMap<Direction,DungeonObject>(Direction.getNbOfDirections());


	/**
	 * Connect this square to the given square in the given direction. 
	 * This method will replace any existing borders in that direction.
	 * 
	 * @param	direction
	 *         	The direction in which the given square should be connected to this square.
	 * @param 	square
	 *          The square that is to be connected to this square.
	 * @throws	IllegalArgumentException
	 * 			If this square cannot border the given square in the given direction
	 * 			or the given square cannot border this square in the direction opposite to the given direction.
	 * 			| ! this.isValidSquareToAdd(direction, square)
	 * 			|		|| ! square.isValidSquareToAdd(direction.getOppositeDirection(), this)
	 * @post    This border will be set.
	 * 			|	new.getBorder(direction) == square && square.getBorder(direction.getOppositeDirection()) == this
	 * @post	The temperature and humidity of this room and the other squares' room will be merged.
	 * 			|	for each {i in new.getAllSquaresInSpace()| true : i.getTemperature()}.equals new.getTemperature()
	 * 			|		&& for each {i in new.getAllSquaresInSpace() | true : i.getHumidityPercentage()}.equals new.getHumidityPercentage()
	 */
	public void connectToSquare(Direction direction, Square square) throws IllegalArgumentException{
		if( ! this.isValidSquareToAdd(direction, square) || ! square.isValidSquareToAdd(direction.getOppositeDirection(), this))
			throw new IllegalArgumentException();
		if (this.hasBorder(direction))
			disconnect(direction);
		if (square.hasBorder(direction.getOppositeDirection()))
			square.disconnect(direction.getOppositeDirection());
		this.setBorder(direction, square);
		square.setBorder(direction.getOppositeDirection(), this);
		resolveSpace();
	}

	/**
	 * Connect this square to the given obstacle in the given direction.
	 * This method will replace any existing borders in that direction.
	 * 
	 * @param	direction
	 *         	The direction in which the obstacle should be connected to this square.
	 * @param 	obstacle
	 * 			The obstacle that is to be connected to this square in the given direction.
	 * @throws	IllegalArgumentException
	 * 			If this square cannot border the given obstacle in the given direction
	 * 			or the obstacle cannot border this square.
	 * 			| ! this.isValidObstacleToAdd(direction, obstacle)
	 * @post	If the given obstacle is already set, nothing will happen, but no exception will be thrown either.
	 * @post	This border will be set.
	 *			|	new.getBorder(direction) == obstacle
	 *			|		&& obstacle.containsSquare(this)
	 * @post	The temperature and humidity of this room and the room behind this obstacle will be merged, if the obstacle is accessible.
	 * 			|	for each {i in new.getAllSquaresInSpace()| true : i.getTemperature()}.equals new.getTemperature()
	 * 			|		&& for each {i in new.getAllSquaresInSpace() | true : i.getHumidityPercentage()}.equals new.getHumidityPercentage()
	 */
	public void connectToObstacle(Direction direction, Obstacle obstacle) throws IllegalArgumentException{
		if(getBorder(direction) != obstacle){
			if(! this.isValidObstacleToAdd(direction, obstacle))
				throw new IllegalArgumentException();
			Square otherSquare = this.getBorderingSquare(direction);
			if(otherSquare != null && obstacle.getFirstSquare()==null 
					&& otherSquare.isValidObstacleToAdd(direction.getOppositeDirection(), obstacle)){
				if(this.getBorder(direction) instanceof Obstacle)
					((Obstacle)getBorder(direction)).deleteSquare(this);
				obstacle.addSquare(otherSquare);
				otherSquare.setBorder(direction.getOppositeDirection(), obstacle);
			} else disconnect(direction);
			if(getBorder(direction) instanceof Obstacle)
				((Obstacle)getBorder(direction)).deleteSquare(this);
			this.setBorder(direction,obstacle);
			obstacle.addSquare(this);
			if ((obstacle.isAccessible()  || (this instanceof RockSquare))&& obstacle.getSecondSquare() != null)
				resolveSpace();
			if(obstacle.getOtherSquare(this) instanceof RockSquare)
				obstacle.getOtherSquare(this).resolveSpace();
		}
	}

	/**
	 * Delete a border from this square in the given direction.
	 * 
	 * @param	direction
	 * 			The direction in which the border needs to be deleted. 
	 * @throws	IllegalArgumentException
	 * 			If the direction isn't a valid direction.
	 * 			| ! isValidDirection(direction)
	 * @throws	IllegalStateException
	 * 			If this square does not agree with given invariants.
	 * @post	Borders will be deleted in both directions.
	 */
	@Raw
	public abstract void disconnect(Direction direction) throws IllegalArgumentException,IllegalStateException; 
	
	/**
	 * All borders will be deleted from this square.
	 * 
	 * @post    All borders will be deleted in both directions.
	 */
	public void disconnect() {
		for(Direction direction: Direction.values())
			disconnect(direction);
	}
	
	/**
	 * Check whether the given square is in the same room as this square.
	 * 
	 * @param 	square
	 * 			The square that needs to be checked whether it is in the same room as this square.
	 * @return 	Whether the given square is in the same room as this square.
	 */
	public abstract boolean isInSameRoom(Square square);
	
	/**
	 * Check whether an avatar can reach the given square square through walking or teleportation.
	 * 
	 * @param 	square
	 * 			The square that needs to be checked whether it can be reached from this square.
	 * @return 	Whether the given square can be reached from this square.
	 */
	public abstract boolean canAccess(Square square);

	/**
	 * Calculates the average temperature of a given list of squares.
	 * 
	 * @param 	squares
	 * 			The given list of squares.
	 * @return	The average temperature of these given squares.
	 */
	public static Temperature getAvgTemperature (List<AccessibleSquare> squares){
		Temperature result = new Temperature(0);
		int size = squares.size();
		if (size == 0)
			return null;
		for(Square square: squares){
			//newTemperature += squareInSpace.getTemperature()/squaresInSpace.size()
			result = new Temperature(result.getTemperatureValue() 
					+ square.getTemperature().getTemperatureValue()/size);
		}
		return result;
	}
	
	/**
	 * Merge the temperature and humidity percentage of all the squares in the room.
	 */
	public abstract void resolveSpace();
	
	/**
	 * All the properties of this square are printed.
	 * Temperatures are printed in the default Celsius scale.
	 */
	public void print(){
		print(TemperatureScale.CELSIUS);
	}
	
	/**
	 * All the properties of this square are printed.
	 * Temperatures are printed in the scale that is given.
	 * 
	 * @param scale	The temperature scale in which the temperatures are expressed.
	 */
	public void print(TemperatureScale scale){
		System.out.println();
		System.out.println("	GENERAL PROPERTIES");
		System.out.println("Temperature: 			" 
				+ this.getTemperature().getTemperatureValue(scale) + scale.getSymbol());
		System.out.println("Humidity Percentage:		" 
				+ this.getHumidityPercentage() + "%");
		System.out.println("Inherently Slippery: 		" 
				+ this.isInherentlySlippery());
		System.out.println();
		System.out.println("	BORDERS"); 
		if(this.getNbOfBorders()==0)
			System.out.print("none");
		else{
			DungeonObject border;
			for(Direction direction: Direction.values()){
				border = getBorder(direction);
				if (border != null){
					System.out.print(direction + " : " + border.getClass().getCanonicalName());
					System.out.println("					");
				}
			}
		}
	}
}


